package com.codingchili.instance.model.spells;

import com.codingchili.instance.context.GameContext;
import com.codingchili.instance.context.Ticker;
import com.codingchili.instance.model.entity.Vector;
import com.codingchili.instance.model.entity.*;
import com.codingchili.instance.model.events.DespawnEvent;
import com.codingchili.instance.model.events.SpawnEvent;
import com.codingchili.instance.model.stats.Attribute;
import com.codingchili.instance.scripting.Bindings;
import com.codingchili.instance.scripting.Scripted;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import com.codingchili.core.logging.Logger;

/**
 * A spell collider in the form of a projectile.
 */
public class Projectile extends SimpleEntity {
    private static final String SPELLS = "spells";
    public static final int EXPLOSION_AREA = 256;
    public static final float DEFAULT_VELOCITY = 64.0f;
    public static final int DEFAULT_SIZE = 16;
    private final Map<String, Object> state = new HashMap<>();
    private final Set<String> hits = new HashSet<>();
    private final AtomicBoolean done = new AtomicBoolean(false);
    private final ActiveSpell active;
    private final GameContext game;
    private final Vector start;
    private final Vector vector;
    private Float ttl = 10_000.0f;
    private boolean explode = false;
    private Ticker update;
    private final Logger logger;

    {
        getModel().setBlocking(true);
    }

    /**
     * Creates a new projectile.
     *
     * @param game   the game context the projectile will be added to.
     * @param active a reference to the spell being casted.
     */
    public Projectile(GameContext game, ActiveSpell active) {
        this.game = game;
        this.active = active;
        this.logger = game.getLogger(getClass());

        var source = active.getSource().getVector();
        var target = active.getTarget().getVector();

        this.hits.add(active.getSource().getId());
        this.vector = source.copy()
                .setAcceleration(1.0f)
                .setDirection(source.copy().deltaY(-24.0f).targetAngle(target))
                .setVelocity(DEFAULT_VELOCITY)
                .setSize(DEFAULT_SIZE);

        this.start = this.vector.copy();

        update = game.ticker(this::tick, GameContext.onAllTicks());
    }

    public Projectile create() {
        var target = active.getTarget().getVector();
        ttl = (start.distance(target) / (vector.getVelocity()) * 1000.0f);
        game.publish(new SpawnEvent(this));
        return this;
    }

    /**
     * Updates the projectile and performs collision checking.
     *
     * @param ticker the ticker object that is updating the projectiles.
     * @return true if the projectile has expired.
     */
    public void tick(Ticker ticker) {
        ttl -= ticker.deltaMS();
        vector.forward(ticker);

        if (ttl <= 0 || range()) {
            range();
            expire();
        } else {
            game.creatures().radius(vector).forEach(creature -> {
                var pass = creature.getStats().get(Attribute.ethereal) >= 1.0f;
                var hit = hits.contains(creature.getId());

                if (!pass && !hit) {
                    Scripted onHit = active.getSpell().getOnSpellActive();

                    if (onHit != null && !done.get()) {
                        if (model.isBlocking()) {
                            expire();
                        }
                        onHit.apply(getBindings().setTarget(creature));
                    }
                    hits.add(creature.getId());
                }
            });
        }
    }

    private boolean range() {
        var distance = start.distance(vector);
        var range = active.getSpell().getRange();
        return distance > range;
    }

    private void expire() {
        game.publish(new DespawnEvent(this));
        done.set(true);

        if (explode) {
            Bindings bindings = getBindings();
            Scripted onHit = active.getSpell().getOnSpellActive();

            game.creatures().radius(vector.copy().setSize(EXPLOSION_AREA)).forEach(creature -> {
                if (onHit != null) {
                    bindings.setTarget(creature);
                    onHit.apply(bindings);
                }
            });
        }
        update.disable();
    }

    /**
     * @return creates the script bindings used for the on-hit effect.
     */
    private Bindings getBindings() {
        Bindings bindings = new Bindings();
        bindings.setSource(active.getSource());
        bindings.put(SPELLS, game.spells());
        bindings.setState(state);
        return bindings;
    }

    /**
     * @param ms time to live for the projectile in ms, if unset the default ttl is {@link #ttl}.
     *           The projectile will expire on collision if the model object is set to blocking.
     * @return fluent.
     */
    public Projectile setTtl(float ms) {
        this.ttl = ms;
        return this;
    }

    /**
     * @return time in milliseconds until the projectile expires.
     */
    public float getTtl() {
        return ttl;
    }

    /**
     * @param explode indicates if the projectile should explode when it expires.
     * @return fluent.
     */
    public Projectile setExplosion(boolean explode) {
        this.explode = explode;
        return this;
    }

    /**
     * @param direction the direction of the projectile.
     * @return fluent.
     */
    public Projectile setDirection(float direction) {
        vector.setDirection(direction);
        return this;
    }

    /**
     * @param velocity the constant speed at which the projectile moves.
     * @return fluent.
     */
    public Projectile setVelocity(float velocity) {
        vector.setVelocity(velocity);
        return this;
    }

    /**
     * @return indicates that this is a projectile, always returns true.
     */
    public Boolean isProjectile() {
        return true;
    }

    /**
     * @return the model object used by the projectile.
     */
    public Model getModel() {
        return model;
    }

    /**
     * @param model the model object used by the projectile.
     */
    public Projectile setModel(Model model) {
        this.model = model;
        return this;
    }

    /**
     * @param size the size of the projectile.
     * @return fluent.
     */
    public Projectile setSize(int size) {
        vector.setSize(size);
        return this;
    }

    /**
     * @return the vector of the projectile.
     */
    public Vector getVector() {
        return vector;
    }
}
